<?php

CRequire('CObject');

/**
	A CTime instance represents a single moment in time, with a maximum precision of one second. CTime can be used to represent abstract "timestamps".
*/
class CTime extends CObject {
	var $_Timestamp;
	
	/**
		Constructor. Creates a new CTime object from the given POSIX timestamp.
		@param timestamp A UNIX timestamp in seconds since the Era (January 1st, 1970 at 0:00).
	*/
	function Init($timestamp) {
		$this->_Timestamp = $timestamp;
	}
	
	/**
		Static. Returns the current time as a CTime instance.
	*/
	function Now() {
		return new CTime(time());
	}
	
	/**
		Creates a new CTime object from a string, which will be parsed using strtotime().
		@param str A UNIX timestamp in seconds since the Era (January 1st, 1970 at 0:00).
	*/
	function FromString($str) {
		return new CTime(strtotime($str));
	}
	
	/**
		Returns a string representing the time of the current instance, formatted as
		desired.
		@param format A string in the same format used by PHP's date() function.
	*/
	function Format($format) {
		return date($format, $this->_Timestamp);
	}
	
	/**
		Returns a new CTime instance, offset from the current CTime instance by the
		specified duration.
		@param duration A CDuration instance representing the offset between times. It can
		be a negative duration (to get a CTime representing the past).
	*/
	function After($duration) {
		return $duration->AddingToTime($this);
	}
	
	/**
		Returns a UNIX timestamp that represents this time (an integer).
	*/
	function AsUNIXTimestamp() { return $this->_Timestamp; }
	
	/**
		Creates a duration that goes from this CTime instance to the
		end of the duration given.
	*/
	function UpTo($time) {
		return new CPeriod($this, $this->DurationUpTo($time));
	}
	
	/**
		Creates a duration that is the difference between this moment
		in time to the given moment. It may be negative. It is always
		absolute (that is, independent of the starting it is applied to).
	*/
	function DurationUpTo($time) {
		return CDuration::Seconds($time->AsUNIXTimestamp() - $this->AsUNIXTimestamp());
	}
	
	/** Creates a CTime instance for the UNIX Epoch. */
	function Epoch() {
		return new CTime(0);
	}
	
	/** Creates a CTime instance for the most recent midnight. */
	function LastMidnight() {
		return new CTime(strtotime('00:00'));
	}
	
	/** Returns a negative value if $time is in the past of this
	CTime object, 0 if it is the same moment,
	or a positive value if it is in its future. */
	function Compare($time) {
		$ts = $time->AsUNIXTimestamp();
		return $this->_Timestamp - $ts;
	}
	
	function __toString() {
		return "<time; timestamp = ".$this->Format('r').">";
	}
}

function CTime($s) { return CTime::FromString($s); }

/**
	A CDuration instance represents a duration, that is, an amount of time between two timestamps. A duration doesn't have a starting or ending point in time, and may represent durations that may be independent of the time they are applied to (ie. "23 minutes" or "one day") or durations that depend upon the time they are applied to (ie. "in one month" or "two years ago").
*/
class CDuration extends CObject {
	/**
		Abstract. Returns the direction this duration will take, encoded as follows: a positive number, if applying this duration to a time will return a time in the future; a negative number, if applying this duration to a time will return a time in the future; zero, if applying this duration to a time will return the exact same time (i.e. this represents a zero-length duration); the value null, if applying this duration to a time may return a time that is either into the past or in the future (i.e. "the nearest Monday").
	*/
	function Direction() {
		CAbstract('CDuration::Direction()', $this);
	}
	
	/**
		Abstract. Returns a duration semantically equal to the current one, but in the past rather than in the future (or if the current duration goes towards the past, in the future rather than in the past).
	*/
	function Invert() {
		CAbstract('CDuration::Invert()', $this);
	}
	
	/** Returns the duration's length in seconds if it is an exact duration,
	false if it's a context-dependent duration. */
	function InSeconds() { return false; }
	
	/** Returns a new duration that is as long as is the sum
	of the receiver's and the argument's lengths. */
	function Adding($x) {
		CAbstract('CDuration::Add()', $this);
	}
	
	/** Returns a duration from a string describing a time or a date. If a date
	is given, the duration will be equal to the lapse in time between it and 
	the most recent midnight (in a positive direction). */
	function FromString($string) {
		$time = CTime::FromString($string);
		$epoch = CTime::LastMidnight();
		return $epoch->DurationUpTo($time);
	}
	
	/** Returns a duration: "$n seconds in the future" (or the past if $n is negative). */
	function Seconds($n) { return new CSecondsDuration($n); }
	/** Returns a duration: "$n minutes in the future" (or the past if $n is negative). */
	function Minutes($n) { return new CSecondsDuration(60 * $n); }
	/** Returns a duration: "$n hours in the future" (or the past if $n is negative). */
	function Hours($n) { return new CSecondsDuration(3600 * $n); }
	/** Returns a duration: "$n days in the future" (or the past if $n is negative). */
	function Days($n) { return new CSecondsDuration(86400 * $n); }
	
	/** Returns a duration: "$n months in the future" (or the past if $n is negative). Note that this is a origin-relative duration. */
	function Months($n) { return new CMonthsDuration($n); }
	/** Returns a duration: "$n years in the future" (or the past if $n is negative). Note that this is a origin-relative duration. */
	function Years($n) { return new CYearsDuration($n); }
}

function CDuration($s) { return CDuration::FromString($s); }

/** Private */
class CSecondsDuration extends CDuration {
	var $_Seconds;
	
	function Init($numberOfSeconds) {
		$this->_Seconds = $numberOfSeconds;
	}
	
	function AddingToTime($time) {
		return new CTime($time->AsUNIXTimestamp() + $this->_Seconds);
	}
	
	function Direction() {
		return $this->_Seconds;
	}
	
	function Invert() {
		return new CSecondsDuration(-($this->_Seconds));
	}
	
	function InSeconds() {
		return $this->_Seconds;
	}
	
	function __toString() {
		return "<exact seconds duration; seconds = {$this->_Seconds}>";
	}
	
	function Adding($duration) {
		return new CSecondsDuration($this->InSeconds() + $duration->InSeconds());
	}
}

/** Private */
class CMonthsDuration extends CDuration {
	var $_Months;
	function Init($months) {
		$this->_Months = $months;
	}
	
	function Direction() { return $this->_Months; }
	
	function AddingToTime($time) {
		$ts = $time->AsUNIXTimestamp();
		$hr = (integer) date('H', $ts);
		$min = (integer) date('i', $ts);
		$sec = (integer) date('s', $ts);
		$day = (integer) date('d', $ts);
		$mon = (integer) date('m', $ts);
		$yr = (integer) date('Y', $ts);
		
		$newts = mktime($hr, $min, $sec, $mon + $this->_Months, $day, $yr);
		return new CTime($newts);
	}
	
	function Invert() {
		return new CMonthsDuration(-($this->_Months));
	}
	
	function __toString() {
		return "<context-dependent months duration; months = {$this->_Months}>";
	}	
}

/** Private */
class CYearsDuration extends CDuration {
	var $_Years;
	function Init($years) {
		$this->_Years = $years;
	}
	
	function Direction() { return $this->_Years; }
	
	function AddingToTime($time) {
		$ts = $time->AsUNIXTimestamp();
		$hr = (integer) date('H', $ts);
		$min = (integer) date('i', $ts);
		$sec = (integer) date('s', $ts);
		$day = (integer) date('d', $ts);
		$mon = (integer) date('m', $ts);
		$yr = (integer) date('Y', $ts);
		
		$newts = mktime($hr, $min, $sec, $mon, $day, $yr + $this->_Years);
		return new CTime($newts);
	}
	
	function Invert() {
		return new CYearsDuration(-($this->_Years));
	}
	
	function __toString() {
		return "<context-dependent years duration; years = {$this->_Years}>";
	}
}

/**
	A period is a length of time, that is, a part of the timeline. Unlike a duration, which is an abstract difference between timestamps, a period has a point of origin (a CTime) and a length (a CDuration) and therefore refers to a fixed period sometime in the past, present or future.
	
	Periods require that their duration be able to know its direction (using the CDuration::Direction() method).
*/
class CPeriod extends CObject {
	var $_Beginning;
	var $_End;
	
	/**
		Constructor. Creates a period starting at $beginning and lasting for $duration.
	*/
	function Init($beginning, $duration) {
		$end = $beginning->After($duration);
		if ($end->Compare($beginning) > 0) {
			$this->_Beginning = $beginning;
			$this->_End = $end;
		} else {
			$this->_Beginning = $end;
			$this->_End = $beginning;
		}
	}
	
	/**
		Returns the beginning time for this period, that is, the moment in time that comes earlier than any other in this period.
	*/
	function PeriodStart() {
		return $this->_Beginning;
	}
	
	/**
		Returns the end time for this period, that is, the moment in time that comes past any other in this period.
	*/
	function PeriodEnd() {
		return $this->_End;
	}

	/**
		Returns the duration of this period. Note that this duration is always in a positive direction (that is, applying it to a date will go towards the future).
		It may not be the same duration used to construct this period and it will always be exact.
	*/
	function Duration() {
		return $this->_Beginning->DurationUpTo($this->_End);
	}
	
	function __toString() {
		$atime = $this->PeriodStart(); $a = $atime->__toString();
		$btime = $this->PeriodEnd(); $b = $btime->__toString();
		return "<period ".get_class($this)." from = $a; to = $b>";
	}
}

// =============
// = Calendars =
// =============

define('kCDay', 'kCDay');
define('kCMonth', 'kCMonth');
define('kCYear', 'kCYear');

/**
	A calendar is an object that is able to turn a specifier for a
	date into a CPeriod.
*/
class CCalendar extends CObject {
	/**
		Returns a CPeriod that matches the given specifier. It can be
		an instance method of a CCalendar, or can be called as a
		static method (in which case it will be called on the built-in
		gregorian calendar).
		
		The non-static version of this method is abstract.
		
		A specifier is an associative array where
		one or more date-part constants are the keys for one or more values (for
		example, kCYear => 1985 or kCMonth => 9).
		
		If a specifier is partial (i.e. it has a kCYear and a kCMonth,
		but not a kCDay) then the returned object will span the
		longest possible period that includes the given items. If
		the period would be nonconsecutive or the combination doesn't
		make sense, null is returned. (For example, kCYear => 1990 and
		kCDay => 1 would be a set of nonconsecutive periods; and kCMonth => 9
		alone doesn't make sense. kCYear => 1992 and kCMonth => 2 will
		span the whole of February 1992.)
		
		No defaults are assumed for any of the date-part constants.
		(For example, omitting kCYear will not automatically use the
		current year.)
		
		The built-in gregorian calendar has a maximum granularity of
		one year and a minimum granularity of one day (where the granularity
		is the time length of the returned periods).
	*/
	function ToPeriod($specs) {
		if (is_null(@$this) || !is_a($this, 'CCalendar')) {
			$cal = &CCalendar::Gregorian();
			return $cal->ToPeriod($specs);
		} else
			CAbstract('CCalendar::ToPeriod()', $this);
	}
	
	/** Returns the built-in gregorian calendar. */
	function &Gregorian() {
		return $GLOBALS['_CGregorianCalendar'];
	}
}

class CGregorianCalendar extends CCalendar {
	function ToPeriod($specs) {
		if (!isset($specs[kCYear]))
			return null;
		
		if (isset($specs[kCMonth]))
			$firstMonth = $lastMonth = $specs[kCMonth];
		else {
			$firstMonth = 1;
			$lastMonth = 12;
		}
		
		if (isset($specs[kCDay]) && !isset($specs[kCMonth]))
			return null;
		
		if (isset($specs[kCDay]))
			$firstDay = $lastDay = $specs[kCDay];
		else {
			$firstDay = 1;
			$lastDay = 0; // thank you mktime()
			$lastMonth++; // for mktime() correctness
		}
		
		$beginning = new CTime(
			mktime(0, 0, 0, $firstMonth, $firstDay, $specs[kCYear])
		);
		$end = new CTime(
			mktime(23, 59, 59, $lastMonth, $lastDay, $specs[kCYear])
		);
		
		return $beginning->UpTo($end);
	}
}

$GLOBALS['_CGregorianCalendar'] = &new CGregorianCalendar();

?>